還記得第一次接手別人寫的程式碼嗎?那種「這是什麼?」的困惑、「為什麼要這樣寫?」的疑問,以及「我該從哪裡開始改?」的無助感。每個開發者都有過這樣的經歷。
經過前九天的學習,我們掌握了 TDD 的基本工具。今天來學習「重構與測試」:在不改變程式外部行為的前提下,改善程式碼內部結構。這就像整理房間一樣,外觀看起來還是同一個房間,但內部變得井然有序、使用起來更方便。
有了測試作為安全網,重構就變得安全而有信心。測試會告訴你:「重構是否保持了原有的行為」。這就像走鋼索時下面有安全網,讓你可以大膽前進。
重構之旅啟程!
├── 第一站:理解重構的本質
├── 第二站:測試驅動的重構實戰
├── 第三站:掌握重構技巧
├── 第四站:重構最佳實踐
└── 終點站:10天學習總結與回顧
今天你將學會:
重構是透過小步驟改善程式碼結構,同時保持程式的外部行為不變。Martin Fowler 在《Refactoring》一書中說:「重構是在不改變軟體可觀察行為的前提下,改善其內部結構」。
很多人常把重構和重寫搞混:
特性 | 重構 | 重寫 |
---|---|---|
改變外部行為 | ❌ 否 | ✅ 可能 |
需要測試保護 | ✅ 必須 | ⚠️ 不一定 |
風險程度 | 低 | 高 |
進行方式 | 小步驟 | 大範圍 |
時間投入 | 持續進行 | 一次性 |
三法則(Rule of Three):
重構的黃金法則:在重構之前,你必須有穩固的測試。沒有測試的重構是危險的,就像沒有安全帶就開車一樣。
1. 確認測試都是綠燈 ✅
2. 執行小步驟重構 🔧
3. 執行測試驗證 🧪
4. 如果測試失敗,立即回復 ↩️
5. 重複直到完成 🔄
讓我們透過實際案例來體驗重構的過程:
建立 src/day10/calculator.ts
export class Calculator {
// 需要重構的複雜函式
calculate(a: number, b: number, operation: string): number {
if (operation === 'add') {
return a + b
} else if (operation === 'subtract') {
return a - b
} else if (operation === 'multiply') {
return a * b
} else if (operation === 'divide') {
if (b === 0) {
throw new Error('Cannot divide by zero')
}
return a / b
} else {
throw new Error('Unknown operation')
}
}
}
建立 tests/day10/calculator.test.ts
import { describe, it, expect } from 'vitest'
import { Calculator } from '../../src/day10/calculator.js'
describe('Calculator Tests', () => {
const calculator = new Calculator()
it('performs basic arithmetic operations', () => {
expect(calculator.calculate(5, 3, 'add')).toBe(8)
expect(calculator.calculate(5, 3, 'subtract')).toBe(2)
expect(calculator.calculate(5, 3, 'multiply')).toBe(15)
expect(calculator.calculate(6, 2, 'divide')).toBe(3)
})
it('handles errors correctly', () => {
expect(() => calculator.calculate(5, 0, 'divide'))
.toThrow('Cannot divide by zero')
expect(() => calculator.calculate(5, 3, 'unknown'))
.toThrow('Unknown operation')
})
})
更新 src/day10/calculator.ts
export class Calculator {
calculate(a: number, b: number, operation: string): number {
const operations: Record<string, () => number> = {
add: () => this.add(a, b),
subtract: () => this.subtract(a, b),
multiply: () => this.multiply(a, b),
divide: () => this.divide(a, b)
}
const operationFn = operations[operation]
if (!operationFn) {
throw new Error('Unknown operation')
}
return operationFn()
}
private add(a: number, b: number): number {
return a + b
}
private subtract(a: number, b: number): number {
return a - b
}
private multiply(a: number, b: number): number {
return a * b
}
private divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Cannot divide by zero')
}
return a / b
}
}
// 重構前:所有邏輯混在一起
function processOrder(order: Order): void {
if (!order.customer) throw new Error('Customer required')
let total = 0
for (const item of order.items) {
total += item.price * item.quantity
}
if (order.customer.type === 'VIP') total *= 0.9
order.total = total
}
// 重構後:清晰分離
function processOrder(order: Order): void {
validateOrder(order)
const total = calculateTotal(order)
const discountedTotal = applyDiscount(total, order.customer)
updateOrder(order, discountedTotal)
}
// 重構前:複雜表達式
function calculatePrice(base: number, type: string, amount: number): number {
return base * (type === 'VIP' ? 0.8 : 1.0) * (amount > 1000 ? 0.95 : 1.0)
}
// 重構後:清晰的變數
function calculatePrice(base: number, type: string, amount: number): number {
const customerDiscount = type === 'VIP' ? 0.8 : 1.0
const volumeDiscount = amount > 1000 ? 0.95 : 1.0
return base * customerDiscount * volumeDiscount
}
建立 tests/day10/remove-duplication.test.ts
import { describe, it, expect } from 'vitest'
describe('Remove Duplication Refactoring', () => {
// 重構後:提取共用驗證
class UserService {
private validateUserId(userId: string): void {
if (!userId || userId.trim() === '') {
throw new Error('Invalid user ID')
}
}
updateEmail(userId: string, email: string): void {
this.validateUserId(userId)
if (!email || !email.includes('@')) {
throw new Error('Invalid email')
}
}
updatePassword(userId: string, password: string): void {
this.validateUserId(userId)
if (!password || password.length < 8) {
throw new Error('Invalid password')
}
}
}
it('validates inputs consistently', () => {
const service = new UserService()
expect(() => service.updateEmail('', 'test@test.com'))
.toThrow('Invalid user ID')
expect(() => service.updateEmail('user123', 'invalid'))
.toThrow('Invalid email')
})
})
每次只改一點點 → 執行測試 → 提交版本控制
$ npm test # 確認綠燈 → 重構 → 再測試 → 提交
getTotal() { // 保留舊介面
console.warn('deprecated')
return this.calculateTotal() // 呼叫新實作
}
✅ 適合:新功能前、修 bug 時、Code Review 時
❌ 不適:截止期近、無測試保護、即將淘汰
恭喜你!完成了 TDD 第一階段的學習。讓我們回顧這 10 天的精彩旅程:
Day 01-03:環境設定、斷言基礎、紅綠重構循環
Day 04-06:測試結構、生命週期、參數化測試
Day 07-09:測試替身、例外處理、覆蓋率分析
Day 10:重構與測試的完美搭配
✅ Vitest 測試框架 | ✅ 斷言方法 | ✅ TDD 循環
✅ 測試組織 | ✅ 生命週期 | ✅ 參數化測試
✅ 測試替身 | ✅ 例外測試 | ✅ 覆蓋率分析 | ✅ 安全重構
第一階段基礎訓練圓滿完成!🎉
今天我們學會了測試驅動的重構,有了測試作為安全網,我們可以大膽地持續改善程式碼。
「寫程式」是為了讓機器理解
「重構」是為了讓人類理解
「測試」是為了讓改變安全
第一階段學習圓滿結束!記住 TDD 精髓:紅 → 綠 → 重構。恭喜完成前十天的 TDD 基礎學習!🚀